From 9ab94f20c200f7e79297b9cbb7d654b8fba67115 Mon Sep 17 00:00:00 2001
From: Fuwn <contact@fuwn.me>
Date: Tue, 2 Apr 2024 22:28:14 -0700
Subject: feat(badges): custom css

---
 src/routes/user/[user]/badges/+page.svelte | 495 +++++++++++++++--------------
 1 file changed, 254 insertions(+), 241 deletions(-)

(limited to 'src/routes/user/[user]/badges/+page.svelte')

diff --git a/src/routes/user/[user]/badges/+page.svelte b/src/routes/user/[user]/badges/+page.svelte
index 8c9e5ebd..78b4cf3e 100644
--- a/src/routes/user/[user]/badges/+page.svelte
+++ b/src/routes/user/[user]/badges/+page.svelte
@@ -3,7 +3,7 @@
 	import { user, type User } from '$lib/Data/AniList/user';
 	import type { Badge } from '$lib/Database/userBadges';
 	// import { domToBlob } from 'modern-screenshot';
-	import { onMount } from 'svelte';
+	import { onDestroy, onMount } from 'svelte';
 	import HeadTitle from '$lib/Home/HeadTitle.svelte';
 	import { databaseTimeToDate, dateToInputTime, inputTimeToDatabaseTime } from '$lib/Utility/time';
 	import root from '$lib/Utility/root.js';
@@ -21,6 +21,8 @@
 	import FallbackImage from '$lib/FallbackImage.svelte';
 	import FallbackBadge from '$lib/FallbackBadge.svelte';
 	import { page } from '$app/stores';
+	import type { UserPreferences } from '$lib/Database/userPreferences.js';
+	import { browser } from '$app/environment';
 	// import { io } from 'socket.io-client';
 
 	export let data;
@@ -51,6 +53,7 @@
 	let importReplies = false;
 	let badger: Partial<User>;
 	let migrateMode = false;
+	let preferences: UserPreferences;
 
 	$: categoryFilter = new URLSearchParams($page.url.searchParams).get('category');
 
@@ -87,6 +90,27 @@
 
 		badgesPromise = fetch(root(`/api/badges?id=${badger.id}`));
 		awcPromise = fetch(proxy(`https://awc.moe/challenger/${badger.name}`));
+		preferences = await (await fetch(root(`/api/preferences?id=${badger.id}`))).json();
+
+		if (preferences.badge_wall_css) {
+			const sanitise = (css: string) =>
+				css
+					.replace(/\/\*[\s\S]*?\*\//g, '')
+					.replace(/<\/?[^>]+(>|$)/g, '')
+					.replace(
+						/(expression|javascript|vbscript|onerror|onload|onclick|onmouseover|onmouseout|onmouseup|onmousedown|onkeydown|onkeyup|onkeypress|onblur|onfocus|onsubmit|onreset|onselect|onchange|ondblclick):/gi,
+						''
+					)
+					.replace(/(behaviour|behavior|moz-binding|content):/gi, '')
+					.replace(/\s+/g, ' ')
+					.trim();
+			const style = document.createElement('style');
+
+			style.dataset.badgeWall = 'true';
+			style.innerHTML = sanitise(preferences.badge_wall_css);
+
+			document.head.appendChild(style);
+		}
 
 		if (data.user && !isId) {
 			currentUserIdentity = userIdentity(data.user);
@@ -103,6 +127,13 @@
 		}
 	});
 
+	onDestroy(() => {
+		if (browser)
+			Array.from(document.head.querySelectorAll('style')).forEach((style) => {
+				if (style.dataset.badgeWall) style.remove();
+			});
+	});
+
 	// const fallback = (event: Event, image: string | undefined) =>
 	// 	setTimeout(() => ((event.target as HTMLImageElement).src = image || ''), 1000);
 
@@ -417,270 +448,252 @@
 			<Skeleton grid={true} count={100} width="150px" height="170px" />
 		{:then badgesResponse}
 			{#if badgesResponse}
-				{#await badgesResponse.json()}
+				{#await badgesResponse.clone().json()}
 					<Message message="Parsing badges ..." />
 
 					<Skeleton grid={true} count={100} width="150px" height="170px" />
 				{:then ungroupedBadges}
-					{#await fetch(root(`/api/preferences?id=${badger.id}`))}
-						<Message message="Loading user preferences ..." />
-
-						<Skeleton grid={true} count={100} width="150px" height="170px" />
-					{:then rawPreferences}
-						{#await rawPreferences.json()}
-							<Message message="Parsing user preferences ..." />
-
-							<Skeleton grid={true} count={100} width="150px" height="170px" />
-						{:then preferences}
-							<div id="badges">
-								{#await awcPromise then badges}
-									{#await badges.text() then text}
-										{@const parsedBadges = awcBadgesGrouped(text)}
-
-										{#if parsedBadges.length > 0}
-											{#each parsedBadges as group}
-												<details open={categoryFilter ? false : true}>
-													<summary>
-														Anime Watching Club <span class="opaque">|</span>
-														{group.group}
-													</summary>
-
-													<p />
-
-													<div class="badges">
-														{#each group.badges as badge}
-															<a
-																href={badge.link}
-																target="_blank"
-																class="badge"
-																id={`badge-${badge.link}`}
-																title={badge.description}
-																use:tooltip
-															>
-																<FallbackImage
-																	source={cdn(thumbnail(badge.image))}
-																	alternative={badge.description}
-																	fallback={thumbnail(badge.image)}
-																/>
-															</a>
-														{/each}
-													</div>
-												</details>
-
-												<p />
-											{/each}
-										{/if}
-									{/await}
-								{/await}
-
-								{#if ungroupedBadges === null}
-									<Message message="Loading badges ..." />
-
-									<Skeleton grid={true} count={10} width="150px" height="170px" />
-								{:else}
-									{@const groupedBadges = Object.entries(groupBadges(ungroupedBadges))}
-
-									{#if isOwner}
-										<div class="card">
-											<button
-												on:click={() => {
-													if (editMode) selectedBadge = undefined;
+					<div id="badges">
+						{#await awcPromise then badges}
+							{#await badges.text() then text}
+								{@const parsedBadges = awcBadgesGrouped(text)}
+
+								{#if parsedBadges.length > 0}
+									{#each parsedBadges as group}
+										<details open={categoryFilter ? false : true}>
+											<summary>
+												Anime Watching Club <span class="opaque">|</span>
+												{group.group}
+											</summary>
 
-													editMode = !editMode;
-												}}
-											>
-												{editMode
-													? $locale().user.badges.editMode.disable
-													: $locale().user.badges.editMode.enable}
-											</button>
-											<span style="margin: 0 0.625rem;">•</span>
-											<button
-												on:click={() => {
-													if (importMode) selectedBadge = undefined;
-
-													importMode = !importMode;
-												}}
-											>
-												{importMode
-													? $locale().user.badges.importMode.disable
-													: $locale().user.badges.importMode.enable}
-											</button>
-											<span style="margin: 0 0.625rem;">•</span>
-											<button
-												on:click={() => {
-													if (migrateMode) selectedBadge = undefined;
-
-													migrateMode = !migrateMode;
-												}}
-											>
-												Migrate Category
-											</button>
+											<p />
 
-											{#if editMode && isOwner}
-												{@const groups = groupedBadges
-													.map((group) => group[0])
-													.filter((group) => group !== 'Uncategorised')}
+											<div class="badges">
+												{#each group.badges as badge}
+													<a
+														href={badge.link}
+														target="_blank"
+														class="badge"
+														id={`badge-${badge.link}`}
+														title={badge.description}
+														use:tooltip
+													>
+														<FallbackImage
+															source={cdn(thumbnail(badge.image))}
+															alternative={badge.description}
+															fallback={thumbnail(badge.image)}
+														/>
+													</a>
+												{/each}
+											</div>
+										</details>
 
-												<p />
+										<p />
+									{/each}
+								{/if}
+							{/await}
+						{/await}
 
-												{#if error}
-													<p style="color: red;">{error}</p>
-												{/if}
+						{#if ungroupedBadges === null}
+							<Message message="Loading badges ..." />
+
+							<Skeleton grid={true} count={10} width="150px" height="170px" />
+						{:else}
+							{@const groupedBadges = Object.entries(groupBadges(ungroupedBadges))}
+
+							{#if isOwner}
+								<div class="card">
+									<button
+										on:click={() => {
+											if (editMode) selectedBadge = undefined;
+
+											editMode = !editMode;
+										}}
+									>
+										{editMode
+											? $locale().user.badges.editMode.disable
+											: $locale().user.badges.editMode.enable}
+									</button>
+									<span style="margin: 0 0.625rem;">•</span>
+									<button
+										on:click={() => {
+											if (importMode) selectedBadge = undefined;
+
+											importMode = !importMode;
+										}}
+									>
+										{importMode
+											? $locale().user.badges.importMode.disable
+											: $locale().user.badges.importMode.enable}
+									</button>
+									<span style="margin: 0 0.625rem;">•</span>
+									<button
+										on:click={() => {
+											if (migrateMode) selectedBadge = undefined;
+
+											migrateMode = !migrateMode;
+										}}
+									>
+										Migrate Category
+									</button>
+
+									{#if editMode && isOwner}
+										{@const groups = groupedBadges
+											.map((group) => group[0])
+											.filter((group) => group !== 'Uncategorised')}
+
+										<p />
+
+										{#if error}
+											<p style="color: red;">{error}</p>
+										{/if}
 
+										<input
+											type="text"
+											placeholder={$locale().user.badges.editMode.imageURL}
+											name="image_url"
+											minlength="1"
+											maxlength="1000"
+											size="15"
+											value={selectedBadge ? selectedBadge.image : ''}
+										/>
+										<input
+											type="text"
+											placeholder={$locale().user.badges.editMode.activityURL}
+											name="activity_url"
+											minlength="1"
+											maxlength="1000"
+											size="15"
+											value={selectedBadge
+												? selectedBadge.post === '#'
+													? ''
+													: selectedBadge.post
+												: ''}
+										/>
+										<input
+											type="text"
+											placeholder={$locale().user.badges.editMode.description}
+											name="description"
+											minlength="1"
+											maxlength="1000"
+											size="15"
+											value={selectedBadge ? selectedBadge.description : ''}
+										/>
+										<Dropdown
+											items={groups.map((group) => ({
+												name: group,
+												url: '#',
+												onClick: () => {
+													const category = document.querySelector('input[name="category"]');
+
+													if (category instanceof HTMLInputElement) category.value = group;
+												}
+											}))}
+											header={false}
+											center={false}
+										>
+											<span slot="title">
 												<input
 													type="text"
-													placeholder={$locale().user.badges.editMode.imageURL}
-													name="image_url"
-													minlength="1"
-													maxlength="1000"
-													size="15"
-													value={selectedBadge ? selectedBadge.image : ''}
-												/>
-												<input
-													type="text"
-													placeholder={$locale().user.badges.editMode.activityURL}
-													name="activity_url"
+													placeholder={$locale().user.badges.editMode.category}
+													name="category"
 													minlength="1"
 													maxlength="1000"
 													size="15"
 													value={selectedBadge
-														? selectedBadge.post === '#'
+														? selectedBadge.category === 'Uncategorised'
 															? ''
-															: selectedBadge.post
+															: selectedBadge.category
 														: ''}
+													list="categories"
 												/>
-												<input
-													type="text"
-													placeholder={$locale().user.badges.editMode.description}
-													name="description"
-													minlength="1"
-													maxlength="1000"
-													size="15"
-													value={selectedBadge ? selectedBadge.description : ''}
-												/>
-												<Dropdown
-													items={groups.map((group) => ({
-														name: group,
-														url: '#',
-														onClick: () => {
-															const category = document.querySelector('input[name="category"]');
-
-															if (category instanceof HTMLInputElement) category.value = group;
-														}
-													}))}
-													header={false}
-													center={false}
-												>
-													<span slot="title">
-														<input
-															type="text"
-															placeholder={$locale().user.badges.editMode.category}
-															name="category"
-															minlength="1"
-															maxlength="1000"
-															size="15"
-															value={selectedBadge
-																? selectedBadge.category === 'Uncategorised'
-																	? ''
-																	: selectedBadge.category
-																: ''}
-															list="categories"
-														/>
-													</span>
-												</Dropdown>
-												<button class="button-lined" on:click={submitBadge}
-													>{selectedBadge
-														? $locale().user.badges.editMode.update
-														: $locale().user.badges.editMode.add}</button
-												>
-												{#if selectedBadge}
-													{$locale().user.badges.editMode.or}
-													<button
-														class="button-lined"
-														on:click={() => {
-															if (selectedBadge) removeBadge(selectedBadge);
-														}}>{$locale().user.badges.editMode.delete}</button
-													>
-												{/if}
-												<span style="float: right;">
-													<input
-														type="datetime-local"
-														value={selectedBadge && selectedBadge.time
-															? dateToInputTime(databaseTimeToDate(selectedBadge.time))
-															: ''}
-													/>
-													<small
-														>Must be full date and time, defaults to now if any fields empty</small
-													>
-												</span>
-											{/if}
-										</div>
-									{/if}
-
-									<p />
-
-									{#if ungroupedBadges.length === 0}
-										<div class="card">
-											No due.moe registered badges found for this user. <a
-												href={'#'}
-												on:click={(e) => e.preventDefault()}
-												title="This alert does not include AWC badges."
-												use:tooltip>?</a
+											</span>
+										</Dropdown>
+										<button class="button-lined" on:click={submitBadge}
+											>{selectedBadge
+												? $locale().user.badges.editMode.update
+												: $locale().user.badges.editMode.add}</button
+										>
+										{#if selectedBadge}
+											{$locale().user.badges.editMode.or}
+											<button
+												class="button-lined"
+												on:click={() => {
+													if (selectedBadge) removeBadge(selectedBadge);
+												}}>{$locale().user.badges.editMode.delete}</button
 											>
-										</div>
+										{/if}
+										<span style="float: right;">
+											<input
+												type="datetime-local"
+												value={selectedBadge && selectedBadge.time
+													? dateToInputTime(databaseTimeToDate(selectedBadge.time))
+													: ''}
+											/>
+											<small>Must be full date and time, defaults to now if any fields empty</small>
+										</span>
 									{/if}
+								</div>
+							{/if}
+
+							<p />
+
+							{#if ungroupedBadges.length === 0}
+								<div class="card">
+									No due.moe registered badges found for this user. <a
+										href={'#'}
+										on:click={(e) => e.preventDefault()}
+										title="This alert does not include AWC badges."
+										use:tooltip>?</a
+									>
+								</div>
+							{/if}
+
+							{#each groupedBadges as [category, badges]}
+								<details open={categoryFilter ? categoryFilter === category : true}>
+									<summary>{category}</summary>
 
-									{#each groupedBadges as [category, badges]}
-										<details open={categoryFilter ? categoryFilter === category : true}>
-											<summary>{category}</summary>
-
-											<p />
+									<p />
 
-											<div class="badges">
-												{#each badges as badge}
-													{#if editMode}
-														<a
-															href={`#`}
-															on:click={() => (selectedBadge = badge)}
-															id={`badge-${badge.id}`}
-															title={`${
-																badge.time
-																	? $locale().dateFormatter(databaseTimeToDate(badge.time))
-																	: ''
-															}${badge.description ? `\n${badge.description}` : ''}`}
-															use:tooltip
-														>
-															<FallbackImage
-																source={cdn(thumbnail(badge.image))}
-																alternative={badge.description}
-																fallback={thumbnail(badge.image)}
-															/>
-														</a>
-													{:else}
-														<FallbackBadge
-															{badge}
-															source={cdn(thumbnail(badge.image))}
-															alternative={badge.description}
-															fallback={thumbnail(badge.image)}
-															hideOnError={preferences.hide_missing_badges}
-														/>
-													{/if}
-												{/each}
-											</div>
-										</details>
+									<div class="badges">
+										{#each badges as badge}
+											{#if editMode}
+												<a
+													href={`#`}
+													on:click={() => (selectedBadge = badge)}
+													id={`badge-${badge.id}`}
+													title={`${
+														badge.time
+															? $locale().dateFormatter(databaseTimeToDate(badge.time))
+															: ''
+													}${badge.description ? `\n${badge.description}` : ''}`}
+													use:tooltip
+												>
+													<FallbackImage
+														source={cdn(thumbnail(badge.image))}
+														alternative={badge.description}
+														fallback={thumbnail(badge.image)}
+													/>
+												</a>
+											{:else}
+												<FallbackBadge
+													{badge}
+													source={cdn(thumbnail(badge.image))}
+													alternative={badge.description}
+													fallback={thumbnail(badge.image)}
+													hideOnError={preferences.hide_missing_badges}
+												/>
+											{/if}
+										{/each}
+									</div>
+								</details>
 
-										{#if groupedBadges[groupedBadges.length - 1][0] !== category}
-											<p />
-										{/if}
-									{/each}
+								{#if groupedBadges[groupedBadges.length - 1][0] !== category}
+									<p />
 								{/if}
-							</div>
-						{:catch}
-							<Popup fullscreen locked>Could not parse badges</Popup>
-						{/await}
-					{:catch}
-						<Popup fullscreen locked>Could not fetch badges</Popup>
-					{/await}
+							{/each}
+						{/if}
+					</div>
 				{:catch}
 					<Popup fullscreen locked>Could not parse badges</Popup>
 				{/await}
-- 
cgit v1.2.3

